Skip to main content

Audits

Buffer protocol has been audited extensively by industry-leading smart contract auditor - Sherlock.xyz

All Audits of Buffer smart contracts can be found below:

v2 Smart Contract Audit by Sherlock (Q4 2022)

https://audits.sherlock.xyz/contests/24

v2.5 Smart Contract Audit by 0x52 (Q3 2023)

  • Review Dates: 7/26/23 - 7/30/23
  • Reviewed by: 0x52 (@IAm0x52)

The Buffer V2.5 repo was reviewed at hash 84b6060b44

In-Scope Contracts

  • contracts/core/AccountRegistrar.sol
  • contracts/core/Booster.sol
  • contracts/core/BufferBinaryOptions.sol
  • contracts/core/BufferBinaryPool.sol
  • contracts/core/BufferRouter.sol
  • contracts/core/CreationWindow.sol
  • contracts/core/OptionMath.sol
  • contracts/core/OptionConfig.sol
  • contracts/core/Validator.sol

Deployment Chain(s)

  • Arbitrum One Mainnet

Summary of Findings

IdentifierTitleSeverityMitigated
[H-01]Payments for coupons to Booster are irretrievableHigh✔️
[H-02]BufferBinaryPool can permanently lock funds on early exerciseHigh✔️
[H-03]Market direction signature can be abused if privateKeeperMode is disabledHigh✔️
[H-04]closeAnytime timestamp is never validated against current timestampHigh✔️
[H-05]closeAnyTime timestamp is never validated against pricing timestampHigh✔️
[M-01]booster#buy fails to apply discountMedium✔️
[M-02]Transferred options can only be closed early by previous ownerMedium✔️
[L-01]Users can buy coupons for options that don't existLow✔️
[L-02]registerAccount sub-call in BufferRouter#openTrades can revert entire transactionLow✔️
[I-01]Booster#buy should support buying multiple boosts at a timeInfo✔️
[I-02]OptionMath#_decay is never usedInfo✔️

[H-01] Payments for coupons to Booster are irretrievable

Details

Booster.sol#L92-L99

    uint256 price = couponPrice - discount;
require(token.balanceOf(user) >= price, "Not enough balance");


token.safeTransferFrom(user, address(this), couponPrice); <- @audit tokens transferred to this
userBoostTrades[tokenAddress][user]
.totalBoostTrades += MAX_TRADES_PER_BOOST;


emit BuyCoupon(tokenAddress, user, couponPrice);

Booster#buy transfers tokens to itself but has no way to recover these tokens, causing them to be permanently trapped in this contract.

Lines of Code

Booster.sol#L80-L100

Recommendation

Transfer fees to a configurable address such as admin

Remediation

Mitigated here by sending fees to admin rather than the booster contract

[H-02] BufferBinaryPool can permanently lock funds on early exercise

Details

BufferBinaryOptions.sol#L380-L395

        profit =
(option.lockedAmount *
OptionMath.blackScholesPriceBinary(
config.iv(),
option.strike,
closingPrice,
option.expiration - closingTime,
true,
isAbove
)) /
1e8;
} else {
profit = option.lockedAmount;
}
pool.send(optionID, user, profit);

When exercising an option early, it is possible to receive a partial payout depending on the factors such as when the contract is unlocked and the current asset price. This will cause the contract to send only a portion of the locked amount.

BufferBinaryPool.sol#L200-L207

    uint256 transferTokenXAmount = tokenXAmount > ll.amount
? ll.amount
: tokenXAmount;


ll.locked = false;
lockedPremium = lockedPremium - ll.premium;
lockedAmount = lockedAmount - transferTokenXAmount;
tokenX.safeTransfer(to, transferTokenXAmount);

This is problematic since the amount of tokens sent is also the amount unlocked. This will leave the remainder of the fees perpetually locked in the BufferBinaryPool contract. Example: A user opens an option that locks 100 USDC. After some time they decide to exercise early. Due to current pricing factors their option is exercised for only 50 USDC. This unlocks 50 USDC leaving the other 50 USDC permanently locked, which means they can never be withdrawn by LPs.

Lines of Code

BufferBinaryPool.sol#L191-L212

Recommendation

BufferBinaryPool#send should always unlock the entire option amount not just the amount sent.

Remediation

Mitigated here. Upon exercise option.lockedAmount is redeemed to the option contract. profit is sent to the user and the remainder (if any) is sent back to the pool.

[H-03] Market direction signature can be abused if privateKeeperMode is disabled

Details

Buffer-Protocol-v2_5/contracts/core/BufferRouter.sol Lines 233 to 246 in 84b6060

if (
!Validator.verifyMarketDirection(|
params,
queuedTrade,
optionInfo.signer
)
) {
emit FailUnlock(
params.optionId,
params.targetContract,
"Router: Wrong market direction"
);
continue;
}
        if (
!Validator.verifyMarketDirection(
params,
queuedTrade,
optionInfo.signer
)
) {
emit FailUnlock(
params.optionId,
params.targetContract,
"Router: Wrong market direction"
);
continue;
}

When options are exercised, the market direction is revealed via a signature provided by the opener. This can cause 2 significant issues if privateKeeperMode is disabled: User can change the direction of their trade after to guarantee they win User can open trade and withhold direction signature to indefinitely lock LP funds

Lines of Code

BufferRouter.sol#L233-L246 BufferRouter.sol#L300-L313

Recommendation

Instead of using a signature at exercise, consider instead concatenating the direction with a salt then hashing storing that hash with the queued trade. Upon closure the salt and direction can be provided and the hash checked against the stored hash.

Remediation

Mitigated here by removing the ability to disable privateKeeperMode

[H-04] closeAnytime timestamp is never validated against current timestamp

Details

BufferRouter.sol#L248-L258

        try
optionsContract.unlock(
params.optionId,
params.closingPrice,
publisherSignInfo.timestamp,
params.isAbove
)
{} catch Error(string memory reason) {
emit FailUnlock(params.optionId, params.targetContract, reason);
continue;
}

When exercising early, the timestamp used for unlocking is extremely important. The current implementation doesn't ever validate the current timestamp is anywhere near the current timestamp. If private keeper mode is disabled, a user could exercise their option in the past when they would get a better payoff.

Lines of Code

BufferRouter.sol#L167-L260

Recommendation

Validate that exercise timestamp is within some margin of the current timestamp

###Remediation Mitigated here by removing the ability to disable privateKeeperMode. This has only been addressed for non-trusted actors and can still be exploited by a malicious/compromised keeper

[H-05] closeAnyTime timestamp is never validated against pricing timestamp

Details

BufferRouter.sol#L248-L258

        try
optionsContract.unlock(
params.optionId,
params.closingPrice,
publisherSignInfo.timestamp,
params.isAbove
)
{} catch Error(string memory reason) {
emit FailUnlock(params.optionId, params.targetContract, reason);
continue;
}

When an option is exercised early, it uses the timestamp of the pricing data without ever checking the timestamp of the closeAnytime signature. This can lead to 2 potential issues: The user may receive less than expected due to the actual exercise timestamp being different than when they signed If private keeper mode is disabled then transactions may be intercepted and the signature used by someone else to close the position with a much different timestamp

Lines of Code

BufferRouter.sol#L167-L260

Recommendation

Pricing data timestamp and closeAnytime timestamp should be required to match.

Remediation

Mitigated here by removing the ability to disable privateKeeperMode. This has only been addressed for non-trusted actors and can still be exploited by a malicious/compromised keeper

[M-01] booster#buy fails to apply discount

Details

Booster.sol#L92-L95

    uint256 price = couponPrice - discount;
re[Title](command:workbench.action.addRootFolder)quire(token.balanceOf(user) >= price, "Not enough balance");

token.safeTransferFrom(user, address(this), couponPrice); <- @audit transfers couponPrice rather than price

When buying boosts the contract mistakenly transfers couponPrice rather than price which is reduced by discount. Additionally the event also emits this incorrect value.

Lines of Code

Booster.sol#L80-L100

Recommendation

Transfer price rather than couponPrice

Remediation

Mitigated here by transferring price rather than couponPrice.

[M-02] Transferred options can only be closed early by previous owner

Details

BufferRouter.sol#L197-L205

        (address signer, ) = getAccountMapping(queuedTrade.user);

bool isUserSignValid = Validator.verifyCloseAnytime(
optionsContract.assetPair(),
closeParam.userSignInfo.timestamp,
params.optionId,
closeParam.userSignInfo.signature,
signer
);

Options are minted as NFTs allowing options to be transferred to other users. This allows users to potentially sell/transfer their options to other user. This cause 2 issues:

  1. The previous owner can close the option early to cause damage to the new owner
  2. The new owner cannot close their option early by themselves

Lines of Code

BufferRouter.sol#L167-L260

Recommendation

NFT functionality should be removed or closeAnytime should determine the signer based on the owner of the option NFT

Remediation

Mitigated here by only allowing positions to be transferred to/from approved addresses.

[L-01] Users can buy coupons for options that don't exist

Details

Booster#buy allows users to buy boosts for any tokens, which means that user can pay for and buy useless boost for tokens that aren't listed.

Lines of Code

Booster.sol#L80-L100

Recommendation

Consider limiting boost purchases to only tokens currently listed for better UX

Remediation

Mitigated here the buy function was refactored to be callable only by owner and use permits for approval which negates the impact of this.

[L-02] registerAccount sub-call in BufferRouter#openTrades can revert entire transaction

Details

BufferRouter.sol#L144-L150

        if (params[index].register.shouldRegister) {
accountRegistrar.registerAccount(
params[index].register.oneCT,
user,
params[index].register.signature
);
}

The openTrades function has been designed to prevent it from reverting under almost every circumstance. The exception to this is that accountRegistrar.registerAccount can revert the entire transaction.

Lines of Code

BufferRouter.sol#L107-L165

Recommendation

Consider using a try-catch block to prevent it from reverting

Remediation

Mitigated here by implementing a try-catch block

[I-01] Booster#buy should support buying multiple boosts at a time

Details

Booster#buy only allows the purchase of one boost at a time. Consider allowing users to bulk buy boosts to save transactions and gas.

Lines of Code

Booster.sol#L80-L100

Recommendation

Consider allowing multiple boosts to be purchased in a single transaction

Remediation

Mitigated here by allowing the user to buy multiple coupons in a single transaction

[I-02] OptionMath#_decay is never used

Details

OptionMath#_decay is never used and should be removed

Lines of Code

OptionMath.sol#L24-L32

Recommendation

OptionMath#_decay should be removed

Remediation

Mitigated here by removing OptionMath#_decay

The core contributor team followed vigorous processes to develop and secure Buffer protocol, including Writing comprehensive unit tests Verifying contracts against written rules (a process known as Formal Verification) Conducting extensive internal code reviews Hiring reputable auditors to audit contracts Running a security bounty program In the future, the core team will audit new versions of the protocol during major upgrades to the architecture of the protocol.